<?php
/**
 * 微信支付V3
 * Author: Jike
 * Date: 2024/1/21
 * Time: 11:09
 */

namespace app\common;


use think\facade\Db;

class WxPayV3
{
    const KEY_LENGTH_BYTE = 32;
    const AUTH_TAG_LENGTH_BYTE = 16;


    /*
     * 支付相关配置
     * ***必填
     * */
    private static array $config = [
        'app_id' => '',             //*** 微信应用AppId
        'merchant_id' => '',        //*** 商户号
        'pay_key' => '',           //*** 支付key
        'notify_url' => '',         // 支付异步通知地址
        'api_cert_path' => '',      // API证书路径,退款和企业付款必填 *** 填写绝对路径 ***
        'api_key_path' => '',       // API证书key路径
        'wxpay_cert_path' => '',       // 微信平台证书路径
    ];

    /**
     * 初始化微信支付配置
     * @param int $id
     * @throws
     * @return void
     * */
    private static function init(int $id = 1):void
    {

        $wxConfig = Db::name('pay_config')->find($id);

        self::$config['merchant_id'] = $wxConfig['merchant_id'] ?? '';// 商户id
        self::$config['pay_key'] = $wxConfig['pay_key'] ?? '';//V3支付key
        self::$config['serial_no'] = $wxConfig['serial_no'] ?? '';//微信平台证书序列号
        self::$config['certificate_serial'] = $wxConfig['certificate_serial'] ?? '';//API证书序列号
        $notify_url = $wxConfig['notify_url'] ?? '';//支付成功通知地址
        $notify_url = $notify_url ? : 'ApiNotify/wxPayV3';
        self::$config['notify_url'] = (string) url($notify_url,[],true,true);//支付通知地址
        self::$config['refund_notify_url'] = url('ApiNotify/wxRefundV3',[],true,true);//退款通知地址
        $cert_path = root_path() . 'cert/';//证书根目录
        self::$config['api_cert_path'] =  $wxConfig['api_cert_path'] ? $cert_path . $wxConfig['api_cert_path'] : '';//api证书
        self::$config['api_key_path'] = $wxConfig['api_key_path'] ? $cert_path . $wxConfig['api_key_path'] : '';//api证书 key
        self::$config['wxpay_cert_path'] = $wxConfig['wxpay_cert_path'] ? $cert_path . $wxConfig['wxpay_cert_path'] : '';//微信平台证书
    }


    /**
     * @notes  微信支付
     * @param   string      $out_trade_no    商户订单号
     * @param   int         $amount        支付金额，单位（分）
     * @param   string      $trade_type   交易类型:JSAPI -JSAPI支付;NATIVE -扫码支付;APP -APP支付,MWEB -H5支付
     * @param   string      $appId       微信应用ID
     * @param   string      $openId       微信应用用户ID
     * @param   string      $desc         商品描述
     * @param   string      $notify_url      通知地址
     * @param   string      $attach         附加数据
     * @param   string      $time_expire      交易结束时间
     * @param   int         $payId        支付配置id
     * @return array
     * @throws
     * @author lj
     * @date 2022/9/27 4:40 下午
     */
    public static function pay(string $trade_type,string $out_trade_no,int $amount,string $appId,string $openId='',string $desc='购买商品',string $notify_url='',string $attach='',string $time_expire='',int $payId = 1):array
    {

        $return_result = [
            'code' => 0,//0成功，其他失败
            'msg' => 'success',
            'data' => []
        ];

        $types = ['JSAPI','NATIVE','APP','MWEB'];
        $trade_type = strtoupper($trade_type);

        try{
            //检测必填参数
            if(!in_array($trade_type,$types)){
                throw new \Exception('trade_type 只能是JSAPI、NATIVE、APP、MWEB其中一种');
            }
            if(!$out_trade_no){
                throw new \Exception('商户订单号必须填写');
            }
            if(!$amount){
                throw new \Exception('支付金额必须填写');
            }

            if($attach && strlen($attach) > 128){
                throw new \Exception("attach 最大长度128字节");
            }

            self::init($payId);
            self::$config['app_id'] = $appId;

            $notify_url = $notify_url ? : self::$config['notify_url'];

            switch($trade_type){
                case 'MWEB':
                    $result = self::mWeb($out_trade_no,$amount,$desc,$notify_url,$time_expire,$attach);
                    break;
                case 'APP':
                    $result = self::app($out_trade_no,$amount,$desc,$notify_url,$time_expire,$attach);
                    break;
                case 'NATIVE':
                    $result = self::native($out_trade_no,$amount,$desc,$notify_url,$time_expire,$attach);
                    break;
                default:
                    $result = self::jsApi($out_trade_no,$openId,$amount,$desc,$notify_url,$time_expire,$attach);
            }

            if($result['code']){
                throw new \Exception($result['msg']);
            }

            $return_result['data'] = $result['data'];
        }catch (\Exception $e){
            $return_result['code'] = 1;
            $return_result['msg'] = $e->getMessage();
        }

        return $return_result;
    }

    /**
     * JSAPI支付
     *
     * @param string $out_trade_no 商户订单号
     * @param string $openId 微信应用用户ID
     * @param string $desc     商品描述
     * @param string $notify_url 通知地址
     * @param string $attach    附加数据
     * @param string $time_expire 交易结束时间
     * @param int $amount 支付金额，单位（分）
     *
     * @throws
     * @return array
     * */
    private static function jsApi(string $out_trade_no,string $openId,int $amount,string $desc,string $notify_url,string $time_expire,string $attach):array
    {
        $return_result = [
            'code' => 0,//0成功，其他失败
            'msg' => 'success',
            'data' => []
        ];

        try{
            //关联参数
            if(!$openId){
                throw new \Exception("统一支付接口中，缺少必填参数openid！");
            }
            $config = self::$config;
            //请求URL
            $url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi';
            //请求方式
            $http_method = 'POST';
            //请求参数
            $param = [
                'mchid' => $config['merchant_id'],
                'appid' => $config['app_id'],
                'description' => $desc,
                'out_trade_no' => $out_trade_no,
                'notify_url' => $notify_url,
                'amount' => [
                    'total' => $amount,
                    'currency' => 'CNY'
                ],
                'payer' => [
                    'openid' => $openId
                ],
                'attach' => $attach
            ];

            if($time_expire){
                $param['time_expire'] = $time_expire;//过期时间，格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE，yyyy-MM-DD表示年月日，T出现在字符串中，表示time元素的开头，HH:mm:ss表示时分秒，TIMEZONE表示时区（+08:00表示东八区时间，领先UTC8小时，即北京时间）。例如：2015-05-20T13:29:35+08:00表示，北京时间2015年5月20日13点29分35秒。
            }

            $token = self::token($url, $http_method, $param); //获取token
            $result = self::https_request($url, $param, $token,$config['serial_no']); //发送请求

            $result_arr = json_decode($result, true);
            //成功返回信息  {"prepay_id":"1030001036201351072852022101201442513049"}
            if (!isset($result_arr['prepay_id'])) { //预支付交易会话标识
                throw new \Exception($result_arr['message'] ?? '网络异常，稍后再试');
            }

            //返回调起支付所需数据
            $timeStamp = getDateTime('U');
            $nonceStr =  self::getNonceStr();
            $package = "prepay_id=" . $result_arr['prepay_id'];
            $signStr = $config['app_id'] . "\n" .
                $timeStamp . "\n" .
                $nonceStr . "\n" .
                $package . "\n" ;
            $data = [
                'appId' => $config['app_id'],
                'timeStamp' => $timeStamp,
                'nonceStr' => $nonceStr,
                'package' => $package,
                'signType' => 'RSA',
                'paySign' => self::MakeSign($signStr),
                'payType' => 'jsapi'
            ];

            $return_result['data'] = $data;
        }catch (\Exception $e){
            $return_result['code'] = 1;
            $return_result['msg'] = $e->getMessage();
        }

        return $return_result;
    }


    /**
     * APP支付
     *
     * @param string $out_trade_no 商户订单号
     * @param string $desc     商品描述
     * @param string $notify_url 通知地址
     * @param string $attach    附加数据
     * @param string $time_expire 交易结束时间
     * @param int $amount 支付金额，单位（分）
     *
     * @throws
     * @return array
     * */
    private static function app(string $out_trade_no,int $amount,string $desc,string $notify_url,string $time_expire,string $attach):array
    {
        $return_result = [
            'code' => 0,//0成功，其他失败
            'msg' => 'success',
            'data' => []
        ];

        try{
            $config = self::$config;
            //请求URL
            $url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/app';
            //请求方式
            $http_method = 'POST';
            //请求参数
            $param = [
                'mchid' => $config['merchant_id'],
                'appid' => $config['app_id'],
                'description' => $desc,
                'out_trade_no' => $out_trade_no,
                'notify_url' => $notify_url,
                'amount' => [
                    'total' => $amount,
                    'currency' => 'CNY'
                ],
                'attach' => $attach
            ];

            if($time_expire){
                $param['time_expire'] = $time_expire;
            }

            $token = self::token($url, $http_method, $param); //获取token
            $result = self::https_request($url, $param, $token,$config['serial_no']); //发送请求

            $result_arr = json_decode($result, true);
            //成功返回信息  {"prepay_id":"1030001036201351072852022101201442513049"}
            if (!isset($result_arr['prepay_id'])) {
                //预支付交易会话标识
                //$return_result['data'] = $result_arr;
                throw new \Exception($result_arr['message'] ?? '网络异常，稍后再试');
            }

            //返回调起支付所需数据
            $timeStamp = (int) getDateTime('U');
            $nonceStr =  self::getNonceStr();
            $prepayId = $result_arr['prepay_id'];
            $signStr = $config['app_id'] . "\n" .
                $timeStamp . "\n" .
                $nonceStr . "\n" .
                $prepayId . "\n" ;
            $data = [
                'appId' => $config['app_id'],
                'partnerId' => $config['merchant_id'],
                'timeStamp' => $timeStamp,
                'nonceStr' => $nonceStr,
                'package' => "Sign=WXPay",
                'prepayId' => $prepayId,
                'paySign' => self::MakeSign($signStr),
                'payType' => 'app'
            ];

            $return_result['data'] = $data;
        }catch (\Exception $e){
            $return_result['code'] = 1;
            $return_result['msg'] = $e->getMessage();
        }

        return $return_result;
    }


    /**
     * MWEB-H5支付
     *
     * @param string $out_trade_no 商户订单号
     * @param string $desc     商品描述
     * @param string $notify_url 通知地址
     * @param string $attach    附加数据
     * @param string $time_expire 交易结束时间
     * @param int $amount 支付金额，单位（分）
     *
     * @throws
     * @return array
     * */
    private static function mWeb(string $out_trade_no,int $amount,string $desc,string $notify_url,string $time_expire,string $attach):array
    {
        $return_result = [
            'code' => 0,//0成功，其他失败
            'msg' => 'success',
            'data' => []
        ];

        try{
            $config = self::$config;
            //请求URL
            $url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/h5';
            //请求方式
            $http_method = 'POST';
            //请求参数
            $param = [
                'mchid' => $config['merchant_id'],
                'appid' => $config['app_id'],
                'description' => $desc,
                'out_trade_no' => $out_trade_no,
                'notify_url' => $notify_url,
                'amount' => [
                    'total' => $amount,
                    'currency' => 'CNY'
                ],
                'scene_info' => [
                    'payer_client_ip' => $_SERVER['REMOTE_ADDR'],
                    'h5_info' => [
                        'type' => 'Wap'
                    ]
                ],
                'attach' => $attach
            ];

            if($time_expire){
                $param['time_expire'] = $time_expire;
            }

            $token = self::token($url, $http_method, $param); //获取token
            $result = self::https_request($url, $param, $token,$config['serial_no']); //发送请求

            $result_arr = json_decode($result, true);
            //成功返回信息  {"h5_url": "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx2916263004719461949c84457c735b0000&package=2150917749"}
            if (!isset($result_arr['h5_url'])) {
                //预支付交易会话标识
                //$return_result['data'] = $result_arr;
                throw new \Exception($result_arr['message'] ?? '网络异常，稍后再试');
            }

            //返回调起支付所需数据
            $return_result['data'] = $result_arr;
        }catch (\Exception $e){
            $return_result['code'] = 1;
            $return_result['msg'] = $e->getMessage();
        }

        return $return_result;
    }

    /**
     * NATIVE-扫码支付
     *
     * @param string $out_trade_no 商户订单号
     * @param string $desc     商品描述
     * @param string $notify_url 通知地址
     * @param string $attach    附加数据
     * @param string $time_expire 交易结束时间
     * @param int $amount 支付金额，单位（分）
     *
     * @throws
     * @return array
     * */
    private static function native(string $out_trade_no,int $amount,string $desc,string $notify_url,string $time_expire,string $attach):array
    {
        $return_result = [
            'code' => 0,//0成功，其他失败
            'msg' => 'success',
            'data' => []
        ];

        try{
            $config = self::$config;
            //请求URL
            $url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/native';
            //请求方式
            $http_method = 'POST';
            //请求参数
            $param = [
                'mchid' => $config['merchant_id'],
                'appid' => $config['app_id'],
                'description' => $desc,
                'out_trade_no' => $out_trade_no,
                'notify_url' => $notify_url,
                'amount' => [
                    'total' => $amount,
                    'currency' => 'CNY'
                ],
                'attach' => $attach
            ];

            if($time_expire){
                $param['time_expire'] = $time_expire;
            }

            $token = self::token($url, $http_method, $param); //获取token
            $result = self::https_request($url, $param, $token,$config['serial_no']); //发送请求

            $result_arr = json_decode($result, true);
            //成功返回信息  {"code_url": "weixin://wxpay/bizpayurl?pr=p4lpSuKzz"}
            if (!isset($result_arr['code_url'])) {
                //预支付交易会话标识
                //$return_result['data'] = $result_arr;
                throw new \Exception($result_arr['message'] ?? '网络异常，稍后再试');
            }

            //返回调起支付所需数据
            $return_result['data'] = $result_arr;
        }catch (\Exception $e){
            $return_result['code'] = 1;
            $return_result['msg'] = $e->getMessage();
        }

        return $return_result;
    }

    /**
     * 微信支付异步通知
     *
     * @param string $Timestamp 时间戳
     * @param string $Nonce     随机字符串
     * @param string $body      请求报文
     * @param string $signature    签名
     * @param string $serialNo    证书序列号
     * @param   int  $payId        支付配置id
     *
     * @throws
     * @return array
     *
     * */
    public static function notify(string $Timestamp,string $Nonce,string $body,string $signature,string $serialNo,int $payId = 1):array
    {
        $return_result = [
            'code' => 0,//0成功，其他失败
            'msg' => 'success',
            'data' => []
        ];

        try{
            if(!$Timestamp){
                throw new \Exception('没有正确的时间戳');
            }

            if(!$Nonce){
                throw new \Exception('没有正确的随机串');
            }

            if(!$body){
                throw new \Exception('没有正确的应答主体');
            }

            self::init($payId);
            $config = self::$config;
            if($config['serial_no'] !== $serialNo){
                throw new \Exception('证书序列号不一致');
            }

            //验签
            $bodyStr = $body;

            $signStr = $Timestamp . "\n" .
                $Nonce . "\n" .
                $bodyStr . "\n" ;

            $signVerify = self::verify($signStr,$signature,$config['wxpay_cert_path']);

            if(!$signVerify){
                throw new \Exception('无效的签名');
            }

            //解密
            $body = json_decode($body,true);
            $verifyArr = [
                ['val'=>'resource'],['val'=>'associated_data','key'=>'resource']
                ,['val'=>'ciphertext','key'=>'resource']
                ,['val'=>'nonce','key'=>'resource']
            ];
            foreach ($verifyArr as $v){
                if(isset($v['key'])){
                    if(!isset($body[$v['key']][$v['val']])){
                        throw new \Exception('缺少必要参数'.$v['val']);
                    }
                }else{
                    if(!isset($body[$v['val']])){
                        throw new \Exception('缺少必要参数'.$v['val']);
                    }
                }
            }

            $data = self::decryptToString($body['resource']['associated_data'],$body['resource']['nonce'],$body['resource']['ciphertext'],$config['pay_key']);

            $return_result['data'] = json_decode($data,true);

            //解密数据如下
            /*
             * {
                    "transaction_id":"1217752501201407033233368018", //微信支付订单号
                    "amount":{ //订单金额信息
                        "payer_total":100,  //用户支付金额
                        "total":100,    //总金额
                        "currency":"CNY",   //货币类型
                        "payer_currency":"CNY"  //用户支付币种
                    },
                    "mchid":"1230000109", //商户号
                    "trade_state":"SUCCESS", //交易状态，枚举值：
                                                SUCCESS：支付成功
                                                REFUND：转入退款
                                                NOTPAY：未支付
                                                CLOSED：已关闭
                                                REVOKED：已撤销（付款码支付）
                                                USERPAYING：用户支付中（付款码支付）
                                                PAYERROR：支付失败(其他原因，如银行返回失败)
                    "bank_type":"CMC", //付款银行
                    "promotion_detail":[ //优惠功能
                        {
                            "amount":100,
                            "wechatpay_contribute":0,
                            "coupon_id":"109519",
                            "scope":"GLOBAL",
                            "merchant_contribute":0,
                            "name":"单品惠-6",
                            "other_contribute":0,
                            "currency":"CNY",
                            "stock_id":"931386",
                            "goods_detail":[
                                {
                                    "goods_remark":"商品备注信息",
                                    "quantity":1,
                                    "discount_amount":1,
                                    "goods_id":"M1006",
                                    "unit_price":100
                                },
                                {
                                    "goods_remark":"商品备注信息",
                                    "quantity":1,
                                    "discount_amount":1,
                                    "goods_id":"M1006",
                                    "unit_price":100
                                }
                            ]
                        },
                        {
                            "amount":100,
                            "wechatpay_contribute":0,
                            "coupon_id":"109519",
                            "scope":"GLOBAL",
                            "merchant_contribute":0,
                            "name":"单品惠-6",
                            "other_contribute":0,
                            "currency":"CNY",
                            "stock_id":"931386",
                            "goods_detail":[
                                {
                                    "goods_remark":"商品备注信息",
                                    "quantity":1,
                                    "discount_amount":1,
                                    "goods_id":"M1006",
                                    "unit_price":100
                                },
                                {
                                    "goods_remark":"商品备注信息",
                                    "quantity":1,
                                    "discount_amount":1,
                                    "goods_id":"M1006",
                                    "unit_price":100
                                }
                            ]
                        }
                    ],
                    "success_time":"2018-06-08T10:34:56+08:00", //支付完成时间
                    "payer":{ //支付者信息
                        "openid":"oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"
                    },
                    "out_trade_no":"1217752501201407033233368018", //商户订单号
                    "appid":"wxd678efh567hg6787", //应用ID
                    "trade_state_desc":"支付成功", //交易状态描述
                    "trade_type":"MICROPAY", //交易类型
                    "attach":"自定义数据",   //附加数据
                    "scene_info":{  //支付场景信息
                        "device_id":"013467007045764"
                    }
             * */

        }catch (\Exception $e){
            $return_result['code'] = 1;
            $return_result['msg'] = $e->getMessage();
        }

        return $return_result;
    }


    /**
     * 退款
     *
     * @param string    $out_refund_no  商户退款单号
     * @param int    $total_amount  原订单金额
     * @param int   $refund_amount  退款金额
     * @param string    $transaction_id 微信支付订单号
     * @param string    $out_trade_no   商户订单号
     * @param string    $notify_url   通知地址
     * @param int    $payId   支付配置id
     *
     * @throws
     * @return array
     *
     * */
    public static function refund(string $out_refund_no,int $total_amount,int $refund_amount,string $transaction_id='',string $out_trade_no='',string $notify_url='',int $payId = 1):array
    {
        $return_result = [
            'code' => 0,//0成功，其他失败
            'msg' => 'success',
            'data' => []
        ];

        try{

            if(!$transaction_id && !$out_trade_no){
                throw new \Exception('微信支付订单号和商户订单号必填一项');
            }

            self::init($payId);
            $config = self::$config;
            //请求URL
            $url = 'https://api.mch.weixin.qq.com/v3/refund/domestic/refunds';
            //请求方式
            $http_method = 'POST';

            $notify_url = $notify_url ? : $config['refund_notify_url'];
            //请求参数
            $param = [
                'out_refund_no' => $out_refund_no,
                'notify_url' => (string) $notify_url,
                'amount' => [
                    'total' => $total_amount,
                    'refund' => $refund_amount,
                    'currency' => 'CNY'
                ]
            ];

            if($transaction_id){
                $param['transaction_id'] = $transaction_id;
            }else{
                $param['out_trade_no'] = $out_trade_no;
            }

            $token = self::token($url, $http_method, $param); //获取token
            $result = self::https_request($url, $param, $token,$config['serial_no']); //发送请求
            //file_put_contents('./refund.txt',$result.PHP_EOL,8);
            $result_arr = json_decode($result, true);
            if(!$result_arr){
                throw new \Exception('网络异常，稍后再试');
            }
            //成功返回信息
            /*
             * {
                  "refund_id": "50000000382019052709732678859",     //微信支付退款单号
                  "out_refund_no": "1217752501201407033233368018",  //商户退款单号
                  "transaction_id": "1217752501201407033233368018", //微信支付订单号
                  "out_trade_no": "1217752501201407033233368018",   //商户订单号
                  "channel": "ORIGINAL",    //退款渠道ORIGINAL：原路退款 BALANCE：退回到余额 OTHER_BALANCE：原账户异常退到其他余额账户 OTHER_BANKCARD：原银行卡异常退到其他银行卡
                  "user_received_account": "招商银行信用卡0403",   //退款入账账户
                  "success_time": "2020-12-01T16:18:12+08:00",  //退款成功时间
                  "create_time": "2020-12-01T16:18:12+08:00",   //退款创建时间
                  "status": "SUCCESS",  //退款状态 SUCCESS：退款成功 CLOSED：退款关闭 PROCESSING：退款处理中 ABNORMAL：退款异常
                  "funds_account": "UNSETTLED", //资金账户
                  "amount": {   //金额信息
                    "total": 100,   //订单金额
                    "refund": 100,  //退款金额
                    "from": [   //退款出资账户及金额
                      {
                        "account": "AVAILABLE",
                        "amount": 444
                      }
                    ],
                    "payer_total": 90,  //用户支付金额
                    "payer_refund": 90, //用户退款金额
                    "settlement_refund": 100,   //应结退款金额
                    "settlement_total": 100,    //应结订单金额
                    "discount_refund": 10,  //优惠退款金额
                    "currency": "CNY"   //退款币种
                  }
                }
             * */
            if (!isset($result_arr['status'])) {
                throw new \Exception($result_arr['message'] ?? '网络异常，稍后再试');
            }

            //返回调起支付所需数据
            $return_result['data'] = $result_arr;
        }catch (\Exception $e){
            $return_result['code'] = 1;
            $return_result['msg'] = $e->getMessage();
        }

        return $return_result;

    }


    /**
     * 查询订单
     *
     * @param string $transaction_id    微信订单号
     * @param string $out_trade_no    商户订单号
     * @param int $payId    支付配置id
     * @return bool
     * */
    public static function queryOrder(string $transaction_id = '',string $out_trade_no = '',int $payId = 1):bool
    {
        try{
            //检测必填参数
            if(!$transaction_id && !$out_trade_no) {
                throw new \Exception("订单查询接口中，out_trade_no、transaction_id至少填一个！");
            }

            self::init($payId);
            $config = self::$config;
            $url = $transaction_id ? 'https://api.mch.weixin.qq.com/v3/pay/transactions/id/'.$transaction_id : ' https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/'.$out_trade_no;
            $url .= '?mchid='.$config['merchant_id'];
            //请求方式
            $http_method = 'GET';

            $token = self::token($url, $http_method, []); //获取token

            $result = self::https_request($url, [], $token,$config['serial_no']); //发送请求
            $result_arr = json_decode($result, true);

            if(!isset($result_arr['trade_state']) || $result_arr['trade_state'] !== 'SUCCESS'){
                throw new \Exception("error");
            }

            $res = true;
        }catch (\Exception $e){
            $res = false;
        }
        return $res;
    }

    /**
     * @notes 商家转账到零钱
     * @param   string $out_batch_no      商户批次单号
     * @param   string $batch_name      批次名称
     * @param   string $batch_remark      批次备注
     * @param   int  $total_amount     转账总金额(分)
     * @param   array $transfer_list  转账明细
     *[{
    "out_detail_no": "x23zy545Bd5436",
    "transfer_amount": 200000,
    "transfer_remark": "2020年4月报销",
    "openid": "o-MYE42l80oelYMDE34nYD456Xoy",
    "user_name": "757b340b45ebef5467rter35gf464344v3542sdf4t6re4tb4f54ty45t4yyry45"
    }]
     * @param string $appId 微信应用ID
     * @param   string  $transfer_scene_id     转账场景ID
     * @param int  $payId //微信支付配置参数id
     * @return array
     * @throws
     * @author lj
     * @date 2022/9/27 4:40 下午
     */
    public static function transfer(string $out_batch_no,string $batch_name,string $batch_remark,int $total_amount,array $transfer_list,string $appId,string $transfer_scene_id='1001',int $payId = 1):array
    {
        $return_result = [
            'code' => 0,//0成功，其他失败
            'msg' => 'success',
            'data' => []
        ];

        try{
            if(!$out_batch_no){
                throw new \Exception('批次单号必须填写！');
            }
            if(!$batch_name){
                throw new \Exception('批次名称必须填写！');
            }
            if(!$batch_remark){
                throw new \Exception('批次备注必须填写！');
            }
            if(!$total_amount){
                throw new \Exception('转账总金额必须填写！');
            }
            if(!count($transfer_list)){
                throw new \Exception('转账明细必须填写！');
            }

            self::init($payId);
            self::$config['app_id'] = $appId;
            $config = self::$config;
            foreach ($transfer_list as &$t){
                if(isset($t['user_name'])){
                    $t['user_name'] = self::getEncrypt($t['user_name']);
                }
            }

            $data = [
                'appid' => $appId,
                'out_batch_no' => $out_batch_no,
                'batch_name' => $batch_name,
                'batch_remark' => $batch_remark,
                'total_amount' => $total_amount,
                'total_num' => count($transfer_list),
                'transfer_detail_list' => $transfer_list,
                'transfer_scene_id' => $transfer_scene_id
            ];

            //请求URL
            $url = 'https://api.mch.weixin.qq.com/v3/transfer/batches';
            //请求方式
            $http_method = 'POST';

            $param = json_encode($data);

            //请求参数
            $token = self::token($url, $http_method, $data); //获取token
            $result = self::https_request($url, $param, $token,$config['serial_no']); //发送请求

            $result_arr = json_decode($result, true);

            if (!isset($result_arr['create_time'])) { //批次受理失败
                throw new \Exception($result_arr['message'] ?? '转账失败');
            }
            //成功返回信息  {"batch_id":"1030001036201351072852022101201442513049","create_time":"2022-10-12T22:08:21+08:00","out_batch_no":"20221011004103000000146822"}
            //批次受理成功，更新提现申请单为提现中状态
            //业务修改为提现中
            $return_result['data'] = $result_arr;
        }catch (\Exception $e){
            $return_result['code'] = 1;
            $return_result['msg'] = $e->getMessage();
        }

        return $return_result;
    }

    /**
     * @notes 商户转账到零钱，商家明细单号查询明细单API
     * @param string $out_batch_no
     * @param string $out_detail_no
     * @param int|string $payId
     * @return array
     * @author lj
     * @date 2022/9/27 5:54 下午
     */
    public static function transferDetails(string $out_batch_no,string $out_detail_no,int $payId = 1):array
    {

        $return_result = [
            'code' => 0,//0成功，其他失败
            'msg' => 'success',
            'data' => []
        ];

        try{
            //请求URL
            $url = 'https://api.mch.weixin.qq.com/v3/transfer/batches/out-batch-no/' . $out_batch_no . '/details/out-detail-no/' . $out_detail_no;
            //请求方式
            $http_method = 'GET';
            //请求参数
            $data = [];

            self::init($payId);

            $token = self::token($url, $http_method, $data); //获取token
            $result = self::https_request($url, $data, $token,self::$config['serial_no']); //发送请求
            //file_put_contents('transfer_detail.txt',$result.PHP_EOL,8);
            $result_arr = json_decode($result, true);

            if(!$result_arr || !isset($result_arr['out_batch_no'])){
                throw new \Exception('网络错误，稍后重试');
            }

            $fail_reason = [
                'ACCOUNT_FROZEN' => '该用户账户被冻结',
                'REAL_NAME_CHECK_FAIL' => '收款人未实名认证，需要用户完成微信实名认证',
                'NAME_NOT_CORRECT' => '收款人姓名校验不通过，请核实信息',
                'OPENID_INVALID' => 'Openid格式错误或者不属于商家公众账号',
                'TRANSFER_QUOTA_EXCEED' => '超过用户单笔收款额度，核实产品设置是否准确',
                'DAY_RECEIVED_QUOTA_EXCEED' => '超过用户单日收款额度，核实产品设置是否准确',
                'MONTH_RECEIVED_QUOTA_EXCEED' => '超过用户单月收款额度，核实产品设置是否准确',
                'DAY_RECEIVED_COUNT_EXCEED' => '超过用户单日收款次数，核实产品设置是否准确',
                'PRODUCT_AUTH_CHECK_FAIL' => '未开通该权限或权限被冻结，请核实产品权限状态',
                'OVERDUE_CLOSE' => '超过系统重试期，系统自动关闭',
                'ID_CARD_NOT_CORRECT' => '收款人身份证校验不通过，请核实信息',
                'ACCOUNT_NOT_EXIST' => '该用户账户不存在',
                'TRANSFER_RISK' => '该笔转账可能存在风险，已被微信拦截',
                'OTHER_FAIL_REASON_TYPE' => '其它失败原因',
                'REALNAME_ACCOUNT_RECEIVED_QUOTA_EXCEED' => '请引导用户在微信支付查看详情',
                'RECEIVE_ACCOUNT_NOT_PERMMIT' => '请在产品设置中调整，添加该用户为收款人',
                'PAYEE_ACCOUNT_ABNORMAL' => '请联系用户完善其在微信支付的身份信息以继续收款',
                'PAYER_ACCOUNT_ABNORMAL' => '可前往商户平台获取解除功能限制指引',
                'TRANSFER_SCENE_UNAVAILABLE' => '该转账场景暂不可用，请确认转账场景ID是否正确',
                'TRANSFER_SCENE_INVALID' => '你尚未获取该转账场景，请确认转账场景ID是否正确',
                'TRANSFER_REMARK_SET_FAIL' => '转账备注设置失败， 请调整后重新再试',
                'RECEIVE_ACCOUNT_NOT_CONFIGURE' => '请前往商户平台-商家转账到零钱-前往功能-转账场景中添加',
                'BLOCK_B2C_USERLIMITAMOUNT_BSRULE_MONTH' => '超出用户单月转账收款20w限额，本月不支持继续向该用户付款',
                'BLOCK_B2C_USERLIMITAMOUNT_MONTH' => '用户账户存在风险收款受限，本月不支持继续向该用户付款',
                'MERCHANT_REJECT' => '商户员工（转账验密人）已驳回转账',
                'MERCHANT_NOT_CONFIRM' => '商户员工（转账验密人）超时未验密',
            ];

            if($result_arr['detail_status'] == 'FAIL'){
                $result_arr['fail_reason_str'] = $fail_reason[$result_arr['fail_reason']] ?? '未知错误';
            }

            /*正确返回
            {
                 "mchid" : "1900001109",
                  "out_batch_no" : "plfk2020042013",
                  "batch_id" : "1030000071100999991182020050700019480001",
                  "appid" : "wxf636efh567hg4356",
                  "out_detail_no" : "x23zy545Bd5436",
                  "detail_id" : "1040000071100999991182020050700019500100",
                  "detail_status" : "SUCCESS",INIT: 初始态。 系统转账校验中
                                    WAIT_PAY: 待确认。待商户确认, 符合免密条件时, 系统会自动扭转为转账中
                                    PROCESSING:转账中。正在处理中，转账结果尚未明确
                                    SUCCESS:转账成功
                                    FAIL:转账失败。需要确认失败原因后，再决定是否重新发起对该笔明细单的转账（并非整个转账批次单）
                  "transfer_amount" : 200000,
                  "transfer_remark" : "2020年4月报销",
                  "fail_reason" : "ACCOUNT_FROZEN",如果转账失败则有失败原因
                                                可选取值：
                                                ACCOUNT_FROZEN: 该用户账户被冻结
                                                REAL_NAME_CHECK_FAIL: 收款人未实名认证，需要用户完成微信实名认证
                                                NAME_NOT_CORRECT: 收款人姓名校验不通过，请核实信息
                                                OPENID_INVALID: Openid格式错误或者不属于商家公众账号
                                                TRANSFER_QUOTA_EXCEED: 超过用户单笔收款额度，核实产品设置是否准确
                                                DAY_RECEIVED_QUOTA_EXCEED: 超过用户单日收款额度，核实产品设置是否准确
                                                MONTH_RECEIVED_QUOTA_EXCEED: 超过用户单月收款额度，核实产品设置是否准确
                                                DAY_RECEIVED_COUNT_EXCEED: 超过用户单日收款次数，核实产品设置是否准确
                                                PRODUCT_AUTH_CHECK_FAIL: 未开通该权限或权限被冻结，请核实产品权限状态
                                                OVERDUE_CLOSE: 超过系统重试期，系统自动关闭
                                                ID_CARD_NOT_CORRECT: 收款人身份证校验不通过，请核实信息
                                                ACCOUNT_NOT_EXIST: 该用户账户不存在
                                                TRANSFER_RISK: 该笔转账可能存在风险，已被微信拦截
                                                OTHER_FAIL_REASON_TYPE: 其它失败原因
                                                REALNAME_ACCOUNT_RECEIVED_QUOTA_EXCEED: 请引导用户在微信支付查看详情
                                                RECEIVE_ACCOUNT_NOT_PERMMIT: 请在产品设置中调整，添加该用户为收款人
                                                PAYEE_ACCOUNT_ABNORMAL: 请联系用户完善其在微信支付的身份信息以继续收款
                                                PAYER_ACCOUNT_ABNORMAL: 可前往商户平台获取解除功能限制指引
                                                TRANSFER_SCENE_UNAVAILABLE: 该转账场景暂不可用，请确认转账场景ID是否正确
                                                TRANSFER_SCENE_INVALID: 你尚未获取该转账场景，请确认转账场景ID是否正确
                                                TRANSFER_REMARK_SET_FAIL: 转账备注设置失败， 请调整后重新再试
                                                RECEIVE_ACCOUNT_NOT_CONFIGURE: 请前往商户平台-商家转账到零钱-前往功能-转账场景中添加
                                                BLOCK_B2C_USERLIMITAMOUNT_BSRULE_MONTH: 超出用户单月转账收款20w限额，本月不支持继续向该用户付款
                                                BLOCK_B2C_USERLIMITAMOUNT_MONTH: 用户账户存在风险收款受限，本月不支持继续向该用户付款
                                                MERCHANT_REJECT: 商户员工（转账验密人）已驳回转账
                                                MERCHANT_NOT_CONFIRM: 商户员工（转账验密人）超时未验密
                  "openid" : "o-MYE42l80oelYMDE34nYD456Xoy",
                  "user_name" : "757b340b45ebef5467rter35gf464344v3542sdf4t6re4tb4f54ty45t4yyry45",
                  "initiate_time" : "2015-05-20T13:29:35.120+08:00",
                  "update_time" : "2015-05-20T13:29:35.120+08:00"
                }*/

            $return_result['data'] = $result_arr;

        }catch (\Exception $e){
            $return_result['code'] = 123;
            $return_result['msg'] = $e->getMessage();
        }
        return $return_result;
    }

    /**
     * @notes 商户转账到零钱，商家明细单号查询明细单API
     * @param string $batch_id
     * @param int|string $payId
     * @return array
     * @author lj
     * @date 2022/9/27 5:54 下午
     */
    public static function transferBatchQuery(string $batch_id,int $payId = 1):array
    {

        $return_result = [
            'code' => 0,//0成功，其他失败
            'msg' => 'success',
            'data' => []
        ];

        try{
            //请求URL
            $url = 'https://api.mch.weixin.qq.com/v3/transfer/batches/batch-id/' . $batch_id;
            //请求方式
            $http_method = 'GET';
            //请求参数
            $data = [];

            self::init($payId);

            $token = self::token($url, $http_method, $data); //获取token
            $result = self::https_request($url, $data, $token,self::$config['serial_no']); //发送请求

            $result_arr = json_decode($result, true);

            if(!$result_arr || !isset($result_arr['transfer_batch'])){
                throw new \Exception('网络错误，稍后重试');
            }

            $fail_reason = [
                'ACCOUNT_FROZEN' => '该用户账户被冻结',
                'REAL_NAME_CHECK_FAIL' => '收款人未实名认证，需要用户完成微信实名认证',
                'NAME_NOT_CORRECT' => '收款人姓名校验不通过，请核实信息',
                'OPENID_INVALID' => 'Openid格式错误或者不属于商家公众账号',
                'TRANSFER_QUOTA_EXCEED' => '超过用户单笔收款额度，核实产品设置是否准确',
                'DAY_RECEIVED_QUOTA_EXCEED' => '超过用户单日收款额度，核实产品设置是否准确',
                'MONTH_RECEIVED_QUOTA_EXCEED' => '超过用户单月收款额度，核实产品设置是否准确',
                'DAY_RECEIVED_COUNT_EXCEED' => '超过用户单日收款次数，核实产品设置是否准确',
                'PRODUCT_AUTH_CHECK_FAIL' => '未开通该权限或权限被冻结，请核实产品权限状态',
                'OVERDUE_CLOSE' => '超过系统重试期，系统自动关闭',
                'ID_CARD_NOT_CORRECT' => '收款人身份证校验不通过，请核实信息',
                'ACCOUNT_NOT_EXIST' => '该用户账户不存在',
                'TRANSFER_RISK' => '该笔转账可能存在风险，已被微信拦截',
                'OTHER_FAIL_REASON_TYPE' => '其它失败原因',
                'REALNAME_ACCOUNT_RECEIVED_QUOTA_EXCEED' => '请引导用户在微信支付查看详情',
                'RECEIVE_ACCOUNT_NOT_PERMMIT' => '请在产品设置中调整，添加该用户为收款人',
                'PAYEE_ACCOUNT_ABNORMAL' => '请联系用户完善其在微信支付的身份信息以继续收款',
                'PAYER_ACCOUNT_ABNORMAL' => '可前往商户平台获取解除功能限制指引',
                'TRANSFER_SCENE_UNAVAILABLE' => '该转账场景暂不可用，请确认转账场景ID是否正确',
                'TRANSFER_SCENE_INVALID' => '你尚未获取该转账场景，请确认转账场景ID是否正确',
                'TRANSFER_REMARK_SET_FAIL' => '转账备注设置失败， 请调整后重新再试',
                'RECEIVE_ACCOUNT_NOT_CONFIGURE' => '请前往商户平台-商家转账到零钱-前往功能-转账场景中添加',
                'BLOCK_B2C_USERLIMITAMOUNT_BSRULE_MONTH' => '超出用户单月转账收款20w限额，本月不支持继续向该用户付款',
                'BLOCK_B2C_USERLIMITAMOUNT_MONTH' => '用户账户存在风险收款受限，本月不支持继续向该用户付款',
                'MERCHANT_REJECT' => '商户员工（转账验密人）已驳回转账',
                'MERCHANT_NOT_CONFIRM' => '商户员工（转账验密人）超时未验密',
            ];

            if($result_arr['detail_status'] == 'FAIL'){
                $result_arr['fail_reason_str'] = $fail_reason[$result_arr['fail_reason']] ?? '未知错误';
            }

            /*正确返回
            {
                 "mchid" : "1900001109",
                  "out_batch_no" : "plfk2020042013",
                  "batch_id" : "1030000071100999991182020050700019480001",
                  "appid" : "wxf636efh567hg4356",
                  "out_detail_no" : "x23zy545Bd5436",
                  "detail_id" : "1040000071100999991182020050700019500100",
                  "detail_status" : "SUCCESS",INIT: 初始态。 系统转账校验中
                                    WAIT_PAY: 待确认。待商户确认, 符合免密条件时, 系统会自动扭转为转账中
                                    PROCESSING:转账中。正在处理中，转账结果尚未明确
                                    SUCCESS:转账成功
                                    FAIL:转账失败。需要确认失败原因后，再决定是否重新发起对该笔明细单的转账（并非整个转账批次单）
                  "transfer_amount" : 200000,
                  "transfer_remark" : "2020年4月报销",
                  "fail_reason" : "ACCOUNT_FROZEN",如果转账失败则有失败原因
                                                可选取值：
                                                ACCOUNT_FROZEN: 该用户账户被冻结
                                                REAL_NAME_CHECK_FAIL: 收款人未实名认证，需要用户完成微信实名认证
                                                NAME_NOT_CORRECT: 收款人姓名校验不通过，请核实信息
                                                OPENID_INVALID: Openid格式错误或者不属于商家公众账号
                                                TRANSFER_QUOTA_EXCEED: 超过用户单笔收款额度，核实产品设置是否准确
                                                DAY_RECEIVED_QUOTA_EXCEED: 超过用户单日收款额度，核实产品设置是否准确
                                                MONTH_RECEIVED_QUOTA_EXCEED: 超过用户单月收款额度，核实产品设置是否准确
                                                DAY_RECEIVED_COUNT_EXCEED: 超过用户单日收款次数，核实产品设置是否准确
                                                PRODUCT_AUTH_CHECK_FAIL: 未开通该权限或权限被冻结，请核实产品权限状态
                                                OVERDUE_CLOSE: 超过系统重试期，系统自动关闭
                                                ID_CARD_NOT_CORRECT: 收款人身份证校验不通过，请核实信息
                                                ACCOUNT_NOT_EXIST: 该用户账户不存在
                                                TRANSFER_RISK: 该笔转账可能存在风险，已被微信拦截
                                                OTHER_FAIL_REASON_TYPE: 其它失败原因
                                                REALNAME_ACCOUNT_RECEIVED_QUOTA_EXCEED: 请引导用户在微信支付查看详情
                                                RECEIVE_ACCOUNT_NOT_PERMMIT: 请在产品设置中调整，添加该用户为收款人
                                                PAYEE_ACCOUNT_ABNORMAL: 请联系用户完善其在微信支付的身份信息以继续收款
                                                PAYER_ACCOUNT_ABNORMAL: 可前往商户平台获取解除功能限制指引
                                                TRANSFER_SCENE_UNAVAILABLE: 该转账场景暂不可用，请确认转账场景ID是否正确
                                                TRANSFER_SCENE_INVALID: 你尚未获取该转账场景，请确认转账场景ID是否正确
                                                TRANSFER_REMARK_SET_FAIL: 转账备注设置失败， 请调整后重新再试
                                                RECEIVE_ACCOUNT_NOT_CONFIGURE: 请前往商户平台-商家转账到零钱-前往功能-转账场景中添加
                                                BLOCK_B2C_USERLIMITAMOUNT_BSRULE_MONTH: 超出用户单月转账收款20w限额，本月不支持继续向该用户付款
                                                BLOCK_B2C_USERLIMITAMOUNT_MONTH: 用户账户存在风险收款受限，本月不支持继续向该用户付款
                                                MERCHANT_REJECT: 商户员工（转账验密人）已驳回转账
                                                MERCHANT_NOT_CONFIRM: 商户员工（转账验密人）超时未验密
                  "openid" : "o-MYE42l80oelYMDE34nYD456Xoy",
                  "user_name" : "757b340b45ebef5467rter35gf464344v3542sdf4t6re4tb4f54ty45t4yyry45",
                  "initiate_time" : "2015-05-20T13:29:35.120+08:00",
                  "update_time" : "2015-05-20T13:29:35.120+08:00"
                }*/

            $return_result['data'] = $result_arr;

        }catch (\Exception $e){
            $return_result['code'] = 123;
            $return_result['msg'] = $e->getMessage();
        }
        return $return_result;
    }

    /**
     * V3下载平台证书
     *
     * @param int $payId //微信支付配置参数id
     *
     * @throws
     * @return string
     *
     * */
    public static function downCert(int $payId = 1):string
    {
        $a = 'composer exec CertificateDownloader.php -- -k 59D04462D3D1883143AA26E67CB57FF6 -m 1617531501 -f ../../../../cert/apiclient_key.pem -s 7266E293DA8F99127BEEAB9A49C9A77C40838392 -o ../../../../cert/';

        try{
            //请求URL
            $url = 'https://api.mch.weixin.qq.com/v3/certificates';

            // 设置参数
            self::init($payId);
            $config = self::$config;
            //请求方式
            $http_method = 'GET';
            $data = [];
            //请求参数
            $token = self::token($url, $http_method, $data); //获取token

            $result_str = self::https_request($url, $data, $token,$config['serial_no']); //发送请求

            //保存证书
            file_put_contents('cert_up.txt', $result_str.PHP_EOL,8);

            $result_arr = json_decode($result_str, true);

            $result_data = $result_arr['data'] ?? [];
            $result = count($result_data) ? array_shift($result_data) : [];

            $serial_no = $result['serial_no'];
            if($serial_no !== $config['serial_no']){
                //保存证书
                $encrypt_certificate = $result['encrypt_certificate'];
                $sign_key = $config['pay_key']; //在API安全中设置
                $cert_content = self::decryptToString($encrypt_certificate['associated_data'], $encrypt_certificate['nonce'], $encrypt_certificate['ciphertext'], $sign_key); //解密

                $cert_path = root_path() . 'cert/';
                $cert_name = 'wechatpay_'.$serial_no.'.pem';

                //保存证书
                file_put_contents($cert_path.$cert_name, $cert_content);

                //更新平台证书
                Db::name('pay_config')->whereIn('id',$payId)->update(['serial_no'=>$serial_no,'wxpay_cert_path'=>$cert_name]);


            }else{
                throw new \Exception('no new certificate');
            }

            $return = $serial_no;

        }catch (\Exception $e){
            $return = $e->getMessage();
        }

        return $return;
    }

    /**
     * @notes 签名生成
     * @param string $url
     * @param string $http_method
     * @param string|array $data
     * @return string
     * @author lj
     * @date 2022/9/27 4:14 下午
     */
    public static function token(string $url,string $http_method, $data):string
    {

        $timestamp = time(); //请求时间戳
        $url_parts = parse_url($url); //获取请求的绝对URL
        $nonce = $timestamp . rand('10000', '99999'); //请求随机串
        $body = is_array($data) ? (empty($data) ? '' : json_encode($data)) : $data; //请求报文主体

        $serial_no = self::$config['certificate_serial']; //证书序列号
        $merchant_id = self::$config['merchant_id']; //商户id
        $canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));

        $message = $http_method . "\n" .
            $canonical_url . "\n" .
            $timestamp . "\n" .
            $nonce . "\n" .
            $body . "\n";

        $sign = self::MakeSign($message); //签名

        $schema = 'WECHATPAY2-SHA256-RSA2048';
        $token = sprintf(
            'mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
            $merchant_id,
            $nonce,
            $timestamp,
            $serial_no,
            $sign
        ); //微信返回token
        return $schema . ' ' . $token;
    }

    /**
     * @notes 发送请求
     * @param string $url
     * @param string|array $data
     * @param string $token
     * @param string $serialNo
     * @throws
     * @return bool|string
     * @author lj
     * @date 2022/9/27 4:15 下午
     */
    public static function https_request(string $url, $data,string $token,string $serialNo=''):string
    {
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_URL, $url);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
        if (!empty($data)) {
            $data = is_array($data) ? json_encode($data) : $data;
            curl_setopt($curl, CURLOPT_POST, 1);
            curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
        }
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
        //添加请求头
        $headers = [
            'Authorization:' . $token,
            'Accept: application/json',
            'Wechatpay-Serial:'.$serialNo,
            'Content-Type: application/json; charset=utf-8',
            'User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36',
        ];
        if (!empty($headers)) {
            curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
        }
        $data = curl_exec($curl);

        //返回结果
        if($data){
            curl_close($curl);
            return $data;
        } else {
            $error = curl_errno($curl);
            curl_close($curl);
            throw new \Exception("curl出错，错误码:$error");
        }

    }

    /**
     * @notes 敏感信息加解密
     * @param string $str
     * @return string
     * @throws
     * @author lj
     * @date 2022/9/27 3:53 下午
     */
    public static function getEncrypt(string $str):string
    {

        //$str是待加密字符串
        $public_key = file_get_contents(self::$config['wxpay_cert_path']);
        $encrypted = '';
        if (openssl_public_encrypt($str, $encrypted, $public_key, OPENSSL_PKCS1_OAEP_PADDING)) {
            //base64编码
            $sign = base64_encode($encrypted);
        } else {
            throw new \Exception('encrypt failed');
        }
        return $sign;
    }


    /**
     *
     * 产生随机字符串，不长于32位
     * @param int $length
     * @return string
     */
    protected static function getNonceStr(int $length = 32):string
    {
        $chars = "abcdefghijklmnopqrstuvwxyz0123456789";
        $str ="";
        $max = strlen($chars) - 1;
        for ( $i = 0; $i < $length; $i++ )  {
            $str .= substr($chars, mt_rand(0, $max), 1);
        }
        return $str;
    }

    /**
     * 生成签名
     * @param  string        $message  签名数据
     * @return string   签名
     * @throws
     */
    private static function MakeSign(string $message):string
    {
        $stream_opts = [
            "ssl" => [
                "verify_peer" => false,
                "verify_peer_name" => false,
            ]
        ];

        $mch_private_key = file_get_contents(self::$config['api_key_path'], false, stream_context_create($stream_opts)); //密钥
        openssl_sign($message, $raw_sign, $mch_private_key, 'sha256WithRSAEncryption');
        return base64_encode($raw_sign); //签名
    }

    /**
     * 签名验证操作
     *
     * @param string $message
     * @param string $signature
     * @param string $merchantPublicKey
     *
     * @throws
     *
     * @return bool
    */
    public static function verify(string $message, string $signature, string $merchantPublicKey):bool
    {
        if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) {
            throw new \Exception("当前PHP环境不支持SHA256withRSA");
        }

        $public_content = openssl_x509_read(file_get_contents($merchantPublicKey));
        $merchant = openssl_get_publickey($public_content);

        $signature = base64_decode($signature);
        return openssl_verify($message, $signature, $merchant, 'sha256WithRSAEncryption');
    }



    /**
     * 解密
     *
     * @param string $associatedData
     * @param string $nonceStr
     * @param string $ciphertext
     * @param string $aesKey
     *
     * @throws
     * @return string
     *
     * */
    private static function decryptToString(string $associatedData,string $nonceStr,string $ciphertext,string $aesKey = ''):string
    {
        $ciphertext = base64_decode($ciphertext);
        if (strlen($ciphertext) <= self::AUTH_TAG_LENGTH_BYTE) {
            return false;
        }
        // ext-sodium (default installed on >= PHP 7.2)
        if (
            function_exists('\sodium_crypto_aead_aes256gcm_is_available') && \sodium_crypto_aead_aes256gcm_is_available()
        ) {
            return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
        }

        // ext-libsodium (need install libsodium-php 1.x via pecl)
        if (
            function_exists('\Sodium\crypto_aead_aes256gcm_is_available') && \Sodium\crypto_aead_aes256gcm_is_available()
        ) {
            return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
        }

        // openssl (PHP >= 7.1 support AEAD)
        if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
            $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
            $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);

            return \openssl_decrypt(
                $ctext,
                'aes-256-gcm',
                $aesKey,
                \OPENSSL_RAW_DATA,
                $nonceStr,
                $authTag,
                $associatedData
            );
        }

        throw new \Exception('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
    }

}
